"""Test blueprints."""

from collections.abc import Iterator
import contextlib
from os import PathLike
import pathlib
from unittest.mock import MagicMock, patch

import pytest

from homeassistant.components import template
from homeassistant.components.blueprint import (
    BLUEPRINT_SCHEMA,
    Blueprint,
    BlueprintInUse,
    DomainBlueprints,
)
from homeassistant.components.template import DOMAIN, SERVICE_RELOAD
from homeassistant.components.template.config import (
    DOMAIN_ALARM_CONTROL_PANEL,
    DOMAIN_BINARY_SENSOR,
    DOMAIN_COVER,
    DOMAIN_FAN,
    DOMAIN_IMAGE,
    DOMAIN_LIGHT,
    DOMAIN_LOCK,
    DOMAIN_NUMBER,
    DOMAIN_SELECT,
    DOMAIN_SENSOR,
    DOMAIN_SWITCH,
    DOMAIN_VACUUM,
    DOMAIN_WEATHER,
)
from homeassistant.const import STATE_ON
from homeassistant.core import Context, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util, yaml as yaml_util

from tests.common import async_mock_service

BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(template.__file__).parent / "blueprints"


@contextlib.contextmanager
def patch_blueprint(
    blueprint_path: str, data_path: str | PathLike[str]
) -> Iterator[None]:
    """Patch blueprint loading from a different source."""
    orig_load = DomainBlueprints._load_blueprint

    @callback
    def mock_load_blueprint(self, path):
        if path != blueprint_path:
            pytest.fail(f"Unexpected blueprint {path}")
            return orig_load(self, path)

        return Blueprint(
            yaml_util.load_yaml(data_path),
            expected_domain=self.domain,
            path=path,
            schema=BLUEPRINT_SCHEMA,
        )

    with patch(
        "homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
        mock_load_blueprint,
    ):
        yield


@contextlib.contextmanager
def patch_invalid_blueprint() -> Iterator[None]:
    """Patch blueprint returning an invalid one."""

    @callback
    def mock_load_blueprint(self, path):
        return Blueprint(
            {
                "blueprint": {
                    "domain": "template",
                    "name": "Invalid template blueprint",
                },
                "binary_sensor": {},
                "sensor": {},
            },
            expected_domain=self.domain,
            path=path,
            schema=BLUEPRINT_SCHEMA,
        )

    with patch(
        "homeassistant.components.blueprint.models.DomainBlueprints._load_blueprint",
        mock_load_blueprint,
    ):
        yield


async def test_inverted_binary_sensor(
    hass: HomeAssistant, device_registry: dr.DeviceRegistry
) -> None:
    """Test inverted binary sensor blueprint."""
    hass.states.async_set("binary_sensor.foo", "on", {"friendly_name": "Foo"})
    hass.states.async_set("binary_sensor.bar", "off", {"friendly_name": "Bar"})

    with patch_blueprint(
        "inverted_binary_sensor.yaml",
        BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml",
    ):
        assert await async_setup_component(
            hass,
            "template",
            {
                "template": [
                    {
                        "use_blueprint": {
                            "path": "inverted_binary_sensor.yaml",
                            "input": {"reference_entity": "binary_sensor.foo"},
                        },
                        "name": "Inverted foo",
                    },
                    {
                        "use_blueprint": {
                            "path": "inverted_binary_sensor.yaml",
                            "input": {"reference_entity": "binary_sensor.bar"},
                        },
                        "name": "Inverted bar",
                    },
                ]
            },
        )

    hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"})
    hass.states.async_set("binary_sensor.bar", "on", {"friendly_name": "Bar"})
    await hass.async_block_till_done()

    assert hass.states.get("binary_sensor.foo").state == "off"
    assert hass.states.get("binary_sensor.bar").state == "on"

    inverted_foo = hass.states.get("binary_sensor.inverted_foo")
    assert inverted_foo
    assert inverted_foo.state == "on"

    inverted_bar = hass.states.get("binary_sensor.inverted_bar")
    assert inverted_bar
    assert inverted_bar.state == "off"

    foo_template = template.helpers.blueprint_in_template(hass, "binary_sensor.foo")
    inverted_foo_template = template.helpers.blueprint_in_template(
        hass, "binary_sensor.inverted_foo"
    )
    assert foo_template is None
    assert inverted_foo_template == "inverted_binary_sensor.yaml"

    inverted_binary_sensor_blueprint_entity_ids = (
        template.helpers.templates_with_blueprint(hass, "inverted_binary_sensor.yaml")
    )
    assert len(inverted_binary_sensor_blueprint_entity_ids) == 2

    assert len(template.helpers.templates_with_blueprint(hass, "dummy.yaml")) == 0

    with pytest.raises(BlueprintInUse):
        await template.async_get_blueprints(hass).async_remove_blueprint(
            "inverted_binary_sensor.yaml"
        )


async def test_reload_template_when_blueprint_changes(hass: HomeAssistant) -> None:
    """Test a template is updated at reload if the blueprint has changed."""
    hass.states.async_set("binary_sensor.foo", "on", {"friendly_name": "Foo"})
    config = {
        DOMAIN: [
            {
                "use_blueprint": {
                    "path": "inverted_binary_sensor.yaml",
                    "input": {"reference_entity": "binary_sensor.foo"},
                },
                "name": "Inverted foo",
            },
        ]
    }
    with patch_blueprint(
        "inverted_binary_sensor.yaml",
        BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml",
    ):
        assert await async_setup_component(hass, DOMAIN, config)

    hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"})
    await hass.async_block_till_done()

    assert hass.states.get("binary_sensor.foo").state == "off"

    inverted = hass.states.get("binary_sensor.inverted_foo")
    assert inverted
    assert inverted.state == "on"

    # Reload the automations without any change, but with updated blueprint
    blueprint_config = yaml_util.load_yaml(
        BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml"
    )
    blueprint_config["binary_sensor"]["state"] = "{{ states(reference_entity) }}"
    with (
        patch(
            "homeassistant.config.load_yaml_config_file",
            autospec=True,
            return_value=config,
        ),
        patch(
            "homeassistant.components.blueprint.models.yaml_util.load_yaml_dict",
            autospec=True,
            return_value=blueprint_config,
        ),
    ):
        await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True)

    hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"})
    await hass.async_block_till_done()

    not_inverted = hass.states.get("binary_sensor.inverted_foo")
    assert not_inverted
    assert not_inverted.state == "off"

    hass.states.async_set("binary_sensor.foo", "on", {"friendly_name": "Foo"})
    await hass.async_block_till_done()

    not_inverted = hass.states.get("binary_sensor.inverted_foo")
    assert not_inverted
    assert not_inverted.state == "on"


async def test_init_attribute_variables_from_blueprint(hass: HomeAssistant) -> None:
    """Test a state based blueprint initializes icon, name, and picture with variables."""
    blueprint = "test_init_attribute_variables.yaml"
    source = "switch.foo"
    entity_id = "sensor.foo"
    hass.states.async_set(source, "on", {"friendly_name": "Foo"})
    config = {
        DOMAIN: [
            {
                "use_blueprint": {
                    "path": blueprint,
                    "input": {"switch": source},
                },
            }
        ],
    }
    assert await async_setup_component(
        hass,
        DOMAIN,
        config,
    )
    await hass.async_block_till_done()

    # Check initial state
    sensor = hass.states.get(entity_id)
    assert sensor
    assert sensor.state == "True"
    assert sensor.attributes["icon"] == "mdi:lightbulb"
    assert sensor.attributes["entity_picture"] == "on.png"
    assert sensor.attributes["friendly_name"] == "Foo"
    assert sensor.attributes["extra"] == "ab"

    hass.states.async_set(source, "off", {"friendly_name": "Foo"})
    await hass.async_block_till_done()

    # Check to see that the template light works
    sensor = hass.states.get(entity_id)
    assert sensor
    assert sensor.state == "False"
    assert sensor.attributes["icon"] == "mdi:lightbulb-off"
    assert sensor.attributes["entity_picture"] == "off.png"
    assert sensor.attributes["friendly_name"] == "Foo"
    assert sensor.attributes["extra"] == "ab"

    # Reload the templates without any change, but with updated blueprint
    blueprint_config = yaml_util.load_yaml(
        pathlib.Path("tests/testing_config/blueprints/template/") / blueprint
    )
    blueprint_config["variables"]["extraa"] = "c"
    blueprint_config["sensor"]["variables"]["extrab"] = "d"
    with (
        patch(
            "homeassistant.config.load_yaml_config_file",
            autospec=True,
            return_value=config,
        ),
        patch(
            "homeassistant.components.blueprint.models.yaml_util.load_yaml_dict",
            autospec=True,
            return_value=blueprint_config,
        ),
    ):
        await hass.services.async_call(DOMAIN, SERVICE_RELOAD, blocking=True)

    sensor = hass.states.get(entity_id)
    assert sensor
    assert sensor.state == "False"
    assert sensor.attributes["icon"] == "mdi:lightbulb-off"
    assert sensor.attributes["entity_picture"] == "off.png"
    assert sensor.attributes["friendly_name"] == "Foo"
    assert sensor.attributes["extra"] == "cd"

    hass.states.async_set(source, "on", {"friendly_name": "Foo"})
    await hass.async_block_till_done()

    sensor = hass.states.get(entity_id)
    assert sensor
    assert sensor.state == "True"
    assert sensor.attributes["icon"] == "mdi:lightbulb"
    assert sensor.attributes["entity_picture"] == "on.png"
    assert sensor.attributes["friendly_name"] == "Foo"
    assert sensor.attributes["extra"] == "cd"


@pytest.mark.parametrize(
    ("blueprint"),
    ["test_event_sensor.yaml", "test_event_sensor_legacy_schema.yaml"],
)
async def test_trigger_event_sensor(
    hass: HomeAssistant,
    device_registry: dr.DeviceRegistry,
    blueprint: str,
) -> None:
    """Test event sensor blueprint."""
    assert await async_setup_component(
        hass,
        "template",
        {
            "template": [
                {
                    "use_blueprint": {
                        "path": blueprint,
                        "input": {
                            "event_type": "my_custom_event",
                            "event_data": {"foo": "bar"},
                        },
                    },
                    "name": "My Custom Event",
                },
            ]
        },
    )

    context = Context()
    now = dt_util.utcnow()
    with patch("homeassistant.util.dt.now", return_value=now):
        hass.bus.async_fire(
            "my_custom_event", {"foo": "bar", "beer": 2}, context=context
        )
        await hass.async_block_till_done()

    date_state = hass.states.get("sensor.my_custom_event")
    assert date_state is not None
    assert date_state.state == now.isoformat(timespec="seconds")
    data = date_state.attributes.get("data")
    assert data is not None
    assert data != ""
    assert data.get("foo") == "bar"
    assert data.get("beer") == 2

    inverted_foo_template = template.helpers.blueprint_in_template(
        hass, "sensor.my_custom_event"
    )
    assert inverted_foo_template == blueprint

    inverted_binary_sensor_blueprint_entity_ids = (
        template.helpers.templates_with_blueprint(hass, blueprint)
    )
    assert len(inverted_binary_sensor_blueprint_entity_ids) == 1

    with pytest.raises(BlueprintInUse):
        await template.async_get_blueprints(hass).async_remove_blueprint(blueprint)


@pytest.mark.parametrize(
    ("blueprint", "override"),
    [
        # Override a blueprint with modern schema with legacy schema
        (
            "test_event_sensor.yaml",
            {"trigger": {"platform": "event", "event_type": "override"}},
        ),
        # Override a blueprint with modern schema with modern schema
        (
            "test_event_sensor.yaml",
            {"triggers": {"platform": "event", "event_type": "override"}},
        ),
        # Override a blueprint with legacy schema with legacy schema
        (
            "test_event_sensor_legacy_schema.yaml",
            {"trigger": {"platform": "event", "event_type": "override"}},
        ),
        # Override a blueprint with legacy schema with modern schema
        (
            "test_event_sensor_legacy_schema.yaml",
            {"triggers": {"platform": "event", "event_type": "override"}},
        ),
    ],
)
async def test_blueprint_template_override(
    hass: HomeAssistant, blueprint: str, override: dict
) -> None:
    """Test blueprint template where the template config overrides the blueprint."""
    assert await async_setup_component(
        hass,
        "template",
        {
            "template": [
                {
                    "use_blueprint": {
                        "path": blueprint,
                        "input": {
                            "event_type": "my_custom_event",
                            "event_data": {"foo": "bar"},
                        },
                    },
                    "name": "My Custom Event",
                }
                | override,
            ]
        },
    )
    await hass.async_block_till_done()

    date_state = hass.states.get("sensor.my_custom_event")
    assert date_state is not None
    assert date_state.state == "unknown"

    context = Context()
    now = dt_util.utcnow()
    with patch("homeassistant.util.dt.now", return_value=now):
        hass.bus.async_fire(
            "my_custom_event", {"foo": "bar", "beer": 2}, context=context
        )
        await hass.async_block_till_done()

    date_state = hass.states.get("sensor.my_custom_event")
    assert date_state is not None
    assert date_state.state == "unknown"

    context = Context()
    now = dt_util.utcnow()
    with patch("homeassistant.util.dt.now", return_value=now):
        hass.bus.async_fire("override", {"foo": "bar", "beer": 2}, context=context)
        await hass.async_block_till_done()

    date_state = hass.states.get("sensor.my_custom_event")
    assert date_state is not None
    assert date_state.state == now.isoformat(timespec="seconds")
    data = date_state.attributes.get("data")
    assert data is not None
    assert data != ""
    assert data.get("foo") == "bar"
    assert data.get("beer") == 2

    inverted_foo_template = template.helpers.blueprint_in_template(
        hass, "sensor.my_custom_event"
    )
    assert inverted_foo_template == blueprint

    inverted_binary_sensor_blueprint_entity_ids = (
        template.helpers.templates_with_blueprint(hass, blueprint)
    )
    assert len(inverted_binary_sensor_blueprint_entity_ids) == 1

    with pytest.raises(BlueprintInUse):
        await template.async_get_blueprints(hass).async_remove_blueprint(blueprint)


async def test_domain_blueprint(hass: HomeAssistant) -> None:
    """Test DomainBlueprint services."""
    reload_handler_calls = async_mock_service(hass, DOMAIN, SERVICE_RELOAD)
    mock_create_file = MagicMock()
    mock_create_file.return_value = True

    with patch(
        "homeassistant.components.blueprint.models.DomainBlueprints._create_file",
        mock_create_file,
    ):
        await template.async_get_blueprints(hass).async_add_blueprint(
            Blueprint(
                {
                    "blueprint": {
                        "domain": DOMAIN,
                        "name": "Test",
                    },
                },
                expected_domain="template",
                path="xxx",
                schema=BLUEPRINT_SCHEMA,
            ),
            "xxx",
            True,
        )
    assert len(reload_handler_calls) == 1


async def test_invalid_blueprint(
    hass: HomeAssistant, caplog: pytest.LogCaptureFixture
) -> None:
    """Test an invalid blueprint definition."""

    with patch_invalid_blueprint():
        assert await async_setup_component(
            hass,
            "template",
            {
                "template": [
                    {
                        "use_blueprint": {
                            "path": "invalid.yaml",
                        },
                        "name": "Invalid blueprint instance",
                    },
                ]
            },
        )

    assert "more than one platform defined per blueprint" in caplog.text
    blueprints = await template.async_get_blueprints(hass).async_get_blueprints()
    assert "invalid.yaml" not in blueprints


async def test_no_blueprint(hass: HomeAssistant) -> None:
    """Test templates without blueprints."""
    with patch_blueprint(
        "inverted_binary_sensor.yaml",
        BUILTIN_BLUEPRINT_FOLDER / "inverted_binary_sensor.yaml",
    ):
        assert await async_setup_component(
            hass,
            "template",
            {
                "template": [
                    {"binary_sensor": {"name": "test entity", "state": "off"}},
                    {
                        "use_blueprint": {
                            "path": "inverted_binary_sensor.yaml",
                            "input": {"reference_entity": "binary_sensor.foo"},
                        },
                        "name": "inverted entity",
                    },
                ]
            },
        )

    hass.states.async_set("binary_sensor.foo", "off", {"friendly_name": "Foo"})
    await hass.async_block_till_done()

    assert (
        len(
            template.helpers.templates_with_blueprint(
                hass, "inverted_binary_sensor.yaml"
            )
        )
        == 1
    )
    assert (
        template.helpers.blueprint_in_template(hass, "binary_sensor.test_entity")
        is None
    )


@pytest.mark.parametrize(
    ("domain", "set_state", "expected"),
    [
        (DOMAIN_ALARM_CONTROL_PANEL, STATE_ON, "armed_home"),
        (DOMAIN_BINARY_SENSOR, STATE_ON, STATE_ON),
        (DOMAIN_COVER, STATE_ON, "open"),
        (DOMAIN_FAN, STATE_ON, STATE_ON),
        (DOMAIN_IMAGE, "test.jpg", "2025-06-13T00:00:00+00:00"),
        (DOMAIN_LIGHT, STATE_ON, STATE_ON),
        (DOMAIN_LOCK, STATE_ON, "locked"),
        (DOMAIN_NUMBER, "1", "1.0"),
        (DOMAIN_SELECT, "option1", "option1"),
        (DOMAIN_SENSOR, "foo", "foo"),
        (DOMAIN_SWITCH, STATE_ON, STATE_ON),
        (DOMAIN_VACUUM, "cleaning", "cleaning"),
        (DOMAIN_WEATHER, "sunny", "sunny"),
    ],
)
@pytest.mark.freeze_time("2025-06-13 00:00:00+00:00")
async def test_variables_for_entity(
    hass: HomeAssistant, domain: str, set_state: str, expected: str
) -> None:
    """Test regular template entities via blueprint with variables defined."""
    hass.states.async_set("sensor.test_state", set_state)
    await hass.async_block_till_done()

    assert await async_setup_component(
        hass,
        "template",
        {
            "template": [
                {
                    "use_blueprint": {
                        "path": f"test_{domain}_with_variables.yaml",
                        "input": {"sensor": "sensor.test_state"},
                    },
                    "name": "Test",
                },
            ]
        },
    )
    await hass.async_block_till_done()

    state = hass.states.get(f"{domain}.test")
    assert state is not None
    assert state.state == expected
